# This file is part of Gehyra.
#
# Gehyra is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Gehyra is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gehyra.  If not, see <http://www.gnu.org/licenses/>.

"""
@package gehyra.entity.modules.Adc.AdcBaseProtocol
Provides a generic implemtation of the ADC BASE protocol.
It will do basic validation on the protocol and drop invalid messages
as per the specification (http://adc.sourceforge.net/versions/ADC-1.0.1.html).

$Id: AdcBaseProtocol.py 452 2011-01-21 13:40:28Z andyhhp@gmail.com $
"""

"""
@file gehyra/entity/modules/Adc/AdcBaseProtocol.py
Provides a generic implemtation of the ADC BASE protocol.
It will do basic validation on the protocol and drop invalid messages
as per the specification (http://adc.sourceforge.net/versions/ADC-1.0.1.html).
"""


import re
from twisted.protocols.basic import LineOnlyReceiver
from gehyra.common.logger import LOG
import socket

class AdcBaseProtocol(LineOnlyReceiver):
    """Advanced Direct Connect BASE Protocol.
    Provides a generic implemtation of the ADC BASE protocol.
    Extends twisted.protocol.basic.LineOnlyReceiver to make the implementation
    simple.

    For the specification
    http://adc.sourceforge.net/versions/ADC-EXT-1.0.5.html#
    _zlib_compressed_communication, consideration should be made about
    supporting the ZLIF extension, at which point this class should extend
    LineReceiver and integrate a zlib decompression system when switching to raw
    mode.  At present, I dont see the point with implementing ZLIF (especially
    as I am not aware of implementation to test against)
    """

    ## line protocol delimiter
    delimiter = '\n'

    ## regex for validating simple_alpha as per the ADC BNF Grammar
    simple_alpha = re.compile(r"^[A-Z]$")
    ## regex for validating simple_alphanum as per the ADC BNF Grammar
    simple_alphanum = re.compile(r"^[A-Z0-9]$")
    ## regex for validating command_name as per the ADC BNF Grammar
    command_name = re.compile(r"^[A-Z][A-Z0-9]{2}$")
    ## regex for validating parameter_name as per the ADC BNF Grammar
    parameter_name = re.compile(r"^[A-Z][A-Z0-9]$")
    ## regex for validating feature (strictly not part of BNF Grammar
    ## but more useful than what is specified)
    feature = re.compile(r"^[+\-][A-Z][A-Z0-9]{3}$")
    ## regex for validating encoded_sid as per the ADC BNF Grammar
    encoded_sid = re.compile(r"^[A-Z2-7]{4}$")
    ## regex for validating encoded_cid as per the ADC BNF Grammar
    encoded_cid = re.compile(r"^[A-Z2-7]+$")

    def __init__(self):
        """Constructor"""
        pass

    def connectionMade(self):
        """Override LineOnlyProtocol.connectionMade to modify the TcpNoDelay 
        status.
        Attempts to set TcpNoDelay and ignores the resulting socket.error if the
        action is not supported.
        """
        try:
            self.transport.setTcpNoDelay(True)
        except socket.error:
            pass

    def sendLine(self, line):
        """Override LineOnlyProtocol.sendLine to log the line in addition to
        sending.
        Logging is provided by the custom logging level PACKET
        @param line String to send onto the tcp connection
        """
        LOG.packet("ADC", " > %s" % line)
        self.transport.writeSequence((line, self.delimiter))

    def lineReceived(self, line):
        """Override LineOnlyProtocol.lineReceived to provide basic ADC support.
        If the line is empty, this serves as a TCP keepalive so should just
        return a blank line packet.  The line is logged using the PACKET logging
        facility.  After that, the line is parsed and validity is tested as per
        the ADC BASE specification.  Lines succeeding the validation test are
        then passed onto self.handle_<CMD>, which is the recomended way of adding
        support for different ADC commands.  If the validation is failed, the
        line is dropped.  If no handle_<CMD> is found, self.handle_unknown is
        called.
        @param line String received from the tcp connection
        """

        if len(line) < 1:
            self.transport.write(self.delimiter)
            return

        LOG.packet("ADC", " < %s" % line)

        mtype, cmd = line[:1], line[1:4]
        rest = list((line[5:]).split(' '))
        parts = {}

        if mtype == 'B':
            parts['my_sid'] = rest[0]
            parts['params'] = rest[1:]

        elif mtype in ['C', 'I', 'H' ]:
            parts['params'] = rest

        elif mtype in ['D', 'E' ]:
            parts['my_sid'] = rest[0]
            parts['target'] = rest[1]
            parts['params'] = rest[2:]

        elif mtype == 'F':
            parts['my_sid'] = rest[0]
            parts['feature'] = []
            parts['params'] = []
            for i in rest[1:]:
                if i[:1] in ['+', '-']:
                    parts['feature'].append(i)
                else:
                    parts['params'].append(i)

        elif mtype == 'U':
            parts['my_cid'] = rest[0]
            parts['params'] = rest[1:]
        else:
            # invalid message type so drop the line
            LOG.debug("Received invalid line from ADC client: %s" % line)
            return

        parts['type'] = mtype
        parts['cmd'] = cmd

        if ('my_sid' in parts and
            self.encoded_sid.search(parts['my_sid']) is None):
            # invalid sid encoding
            LOG.debug("Invalid SID enocding from ADC client: %s"
                      % parts['my_sid'])
            return

        if ('target' in parts and
            self.encoded_sid.search(parts['target']) is None):
            # invalid target sid encoding
            LOG.debug("Invalid target SID encoding from ADC client: %s"
                      % parts['target'])

        if ('my_cid' in parts and
            self.encoded_cid.search(parts['my_cid']) is None):
            # invalid cid encoding
            LOG.debug("Invalid CID encoding from ADC client: %s"
                      % parts['my_cid'])

        if 'features' in parts:
            for f in parts['features']:
                if self.feature.search(f) is None:
                    # invalid feature
                    LOG.debug("Invalid feature from ADC client: %s" % f)
                    return

        getattr(self, "handle_%s" % cmd, self.handle_unknown)(**parts)

    def handle_unknown(self, **parts):
        """Handle unknown ADC commands.
        Defaults to logging and dropping the packet.  Subclasses should override
        this to change the default behaviour.
        @param **parts Dictionary of parsed parts of the line
        """
        LOG.debug("In AdcBaseProtocol.handle_unknown for command %s"
                  % parts['cmd'])
